package com.squeekeek.util.qrcode.Core;

import java.util.ArrayList;
import java.util.List;

import org.opencv.core.Core;
import org.opencv.core.Mat;
import org.opencv.core.Point;
import org.opencv.core.Scalar;

import com.squeekeek.util.qrcode.QRErrorCorrection;

import android.util.Log;

public class QRDataDecode {
	/**
	 * <p>
	 * Predict the version of the QR Code.
	 * </p>
	 * 
	 * @param finderCenters An instance of the three finder patterns of the QR code
	 * @param modWidth The estimated module width
	 * @return The predicted version number
	 */
	public static int predictVersion(QRFinderPattern finderCenters, double modWidth) {
		double codeWidth = Core.norm(OpenCVExtras.subtractPoints2D(finderCenters.getPosition(QRFinderPattern.POS_UR), finderCenters.getPosition(QRFinderPattern.POS_LL)), Core.NORM_L2) / Math.sqrt(2);
		
		return (int) Math.round( ((codeWidth / modWidth) - 10) / 4 );
	}
	
	/**
	 * <p>
	 * Get an estimated module width for the QR Code.
	 * </p>
	 * 
	 * @param inImg Image matrix to process
	 * @param finderCenters An instance of the three finder patterns of the QR
	 * Code
	 * @return The estimated module width
	 */
	public static double getModuleWidth(Mat inImg, QRFinderPattern finderCenters) {
		int currentState;
		int pixVal;
		int totalPixels;
		int modSize = 0;
		int[] statePixels = {0, 0, 0};
		Point[] myPoints = finderCenters.getArray();
		
		for(int i = 0; i < 3; i++) {
			currentState = 0;
			
			for(int j = (int) myPoints[i].x; j < inImg.cols(); j++) {
				pixVal = (int) inImg.get((int) myPoints[i].y, j)[0];
				
				if((currentState == 2) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					totalPixels = 0;
					
					for(int k : statePixels) {
						totalPixels += k;
					}
					
					modSize += totalPixels;
					break;
				}
				else if((currentState == 0) && (pixVal == QRBinarize.PIXEL_WHITE)) {
					currentState++;
				}
				else if((currentState == 1) && (pixVal == QRBinarize.PIXEL_BLACK)) {
					currentState++;
				}
				
				statePixels[currentState]++;
			}
		}
		
		return modSize / 21.0;
	}
	
	/**
	 * <p>
	 * Get the format information of the QR Code
	 * </p>
	 * <p>
	 * This function gets the format information of the QR Code. This
	 * also includes the error correction necessary.
	 * </p>
	 * 
	 * @param inImg Image matrix to process
	 * @param finderCenters An instance of the three finder patterns of the
	 * QR Code
	 * @param modWidth The estimated module width
	 * @return 5-bit format data
	 */
	public static int getFormat(Mat inImg, QRFinderPattern finderCenters, double modWidth) {
		int pixVal;
		int offset = 0;
		int fmtInfo = 0;
		int fmtInfoExtra = 0;
		Point[] points = finderCenters.getArray();
		
		for(int i = 0; i < 17; i++) {
			if(i == 6 || i == 10) {
				// Skip these pixels
				i++;
				offset++;
			}
			
			if(i <= 8) {
				pixVal = (int) inImg.get((int) (points[0].y - (3 - i) * modWidth), (int) (points[0].x + 5 * modWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfo |= 0x00 << (i - offset);
				}
				else {
					fmtInfo |= 0x01 << (i - offset);
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(0).x + 5 * squareWidth, imgFinderLoc.get(0).y - (3 - i) * squareWidth), 1, new Scalar(0x70), -1);
			}
			else {
				pixVal = (int) inImg.get((int) (points[0].y + 5 * modWidth), (int) (points[0].x + (4 + (9 - i)) * modWidth))[0];

				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfo |= 0x00 << (i - offset);
				}
				else {
					fmtInfo |= 0x01 << (i - offset);
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(0).x + (4 + (9 - i)) * squareWidth, imgFinderLoc.get(0).y + 5 * squareWidth), 1, new Scalar(0x70), -1);
			}
		}
		
		// Reset offset
		offset = 0;
		
		// Read format info at upper-right and lower-left parts of the barcode
		for(int i = 0; i < 16; i++) {
			if(i == 8) {
				i++;
				offset++;
			}
			
			if(i < 8) {
				pixVal = (int) inImg.get((int) (points[2].y + 5 * modWidth), (int) (points[2].x + (3 - i) * modWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfoExtra |= 0x00 << i;
				}
				else {
					fmtInfoExtra |= 0x01 << i;
				}
				
				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(2).x + (3 - i) * squareWidth, imgFinderLoc.get(2).y + 5 * squareWidth), 1, new Scalar(0x70), -1);
			}
			else {
				pixVal = (int) inImg.get((int) (points[1].y - (3 + (9 - i)) * modWidth), (int) (points[1].x + 5 * modWidth))[0];
				
				// Read as a '0' if it's a white module
				if(pixVal > 128) {
					fmtInfoExtra |= 0x00 << (i - offset);
				}
				else {
					fmtInfoExtra |= 0x01 << (i - offset);
				}

				// Core.circle(imgMatBW, new Point(imgFinderLoc.get(1).x + 5 * squareWidth, imgFinderLoc.get(1).y - (3 + (9 - i)) * squareWidth), 1, new Scalar(0x70), -1);
			}
		}
		
		return fmtInfo != -1 ? fmtInfo : fmtInfoExtra;
	}
	
	/**
	 * <p>
	 * Reads the data portion of the QR Code.
	 * </p>
	 * 
	 * @param inImg Image matrix to process
	 * @param finderCenters An instance of three finder patterns of a QR Code
	 * @param modWidth Estimated module width
	 * @param format 5-bit format string
	 * @param version QR Code version
	 * @return A list containing the raw data stream with interlaced data and
	 * error correction codewords
	 */
	public static List <Byte> getData(Mat inImg, QRFinderPattern finderCenters, double modWidth, int format, int version) {
		int pixVal;
		int bytePos = 0;
		boolean readUp = true;
		byte dataByte = 0;
		Point[] points = finderCenters.getArray();
		List <Byte> dataStream = new ArrayList <Byte> ();
		
		// Start at lower-right portion and read alternately upwards and downwards
		// (i, j) -> (col, row)
		int j = (int) Math.round(points[1].y + 3 * modWidth);
		
		for(int i = (int) Math.round(points[2].x + 3 * modWidth); i >= (int) Math.round(points[0].x - 3 * modWidth); i -= Math.round(2 * modWidth)) {

			// Reverse reading direction
			if(isQuietZone(finderCenters, modWidth, i, j)) {
				readUp = !readUp;
			}
			
			// Skip rows with functional parts until you get to the data encoding part
			while(isQuietZone(finderCenters, modWidth, i, j) || isFunctional(finderCenters, modWidth, version, i, j)) {
				if(readUp) {
					j -= (int) Math.round(modWidth);
				}
				else {
					j += (int) Math.round(modWidth);
				}
				
				if(j < 0 || j > inImg.rows()) {
					Log.d("QRCode", "Anyare? Locked at quiet zone...");
					return null;
				}
			}
			
			// Start data reading
			while(!isQuietZone(finderCenters, modWidth, i, j)) {
				
				// Check horizontal timing pattern
				if(j == (int) Math.round(points[0].y + 3 * modWidth)) {
					// Skip this line
					if(readUp) {
						j -= (int) Math.round(modWidth);
					}
					else {
						j += (int) Math.round(modWidth);
					}
				}
				
				// Read right. Check for vertical timing pattern
				if(!isFunctional(finderCenters, modWidth, version, i, j) && !isQuietZone(finderCenters, modWidth, i, j) && (i != (int) Math.round(points[0].x + 3 * modWidth))) {
					pixVal = (int) inImg.get(j, i)[0];
					Core.circle(inImg, new Point(i, j), 2, new Scalar(0x45), -1);
					
					// Check white pixel
					if(pixVal > 128) {
						dataByte |= (maskDataBit(finderCenters, modWidth, format, i, j) ? 0x01 : 0x00) << (7 - bytePos);
					}
					else {
						dataByte |= (maskDataBit(finderCenters, modWidth, format, i, j) ? 0x00 : 0x01) << (7 - bytePos);
					}
					
					if(bytePos % 8 == 7) {
						dataStream.add(dataByte);
						
						dataByte = 0;
						bytePos = 0;
					}
					else {
						bytePos++;
					}
				}

				// Read left. Check for vertical timing pattern
				i -= (int) Math.round(modWidth);
				
				if(!isFunctional(finderCenters, modWidth, version, i, j) && !isQuietZone(finderCenters, modWidth, i, j) && (i != (int) Math.round(points[0].x + 3 * modWidth))) {
					pixVal = (int) inImg.get(j, i)[0];
					Core.circle(inImg, new Point(i, j), 2, new Scalar(0x45), -1);
					
					// Check white pixel
					if(pixVal > 128) {
						dataByte |= (maskDataBit(finderCenters, modWidth, format, i, j) ? 0x01 : 0x00) << (7 - bytePos);
					}
					else {
						dataByte |= (maskDataBit(finderCenters, modWidth, format, i, j) ? 0x00 : 0x01) << (7 - bytePos);
					}
					
					if(bytePos == 7) {
						dataStream.add(dataByte);
						
						dataByte = 0;
						bytePos = 0;
					}
					else {
						bytePos++;
					}
				}
				
				// Revert pointer to right
				i += (int) Math.round(modWidth);
				
				if(readUp) {
					j -= (int) Math.round(modWidth);
				}
				else {
					j += (int) Math.round(modWidth);
				}
			}
			
			// By then, pointer should have gone at the quiet zone
		}
		
		return dataStream;
	}
	
	/**
	 * <p>
	 * Isolates the data portion of the QR Code and deinterlaces it
	 * from the error correction codewords.
	 * </p>
	 * 
	 * @param dataStream Raw data stream read
	 * @param dataBlocks Array containing the number of codewords in each data block
	 * @return A list containing the deinterlaced data stream
	 * @see #getData
	 * @see QRErrorCorrection#getBlockData
	 */
	public static List <Byte> getDataStream(List <Byte> dataStream, byte[] dataBlocks) {
		int lastDiff = 0;
		byte prevBlock = -1;
		List <Byte> dataWords = new ArrayList <Byte> ();
		
		// Deinterlace data codewords
		for(int i = 0; i < dataBlocks.length; i++) {
			if(prevBlock != -1 && prevBlock != dataBlocks[i]) {
				lastDiff = i;
			}
			
			for(int j = 0; j < dataBlocks[i]; j++) {
				if(j == dataBlocks[i] - 1) {
					// Offset for 5Q and 5H QR Codes
					dataWords.add(dataStream.get(i + j * dataBlocks.length - lastDiff));
				}
				else {
					dataWords.add(dataStream.get(i + j * dataBlocks.length));
				}
			}

			prevBlock = dataBlocks[i];
		}
		
		return dataWords;
	}
	
	/**
	 * <p>
	 * Isolates the error correction codewords from the QR Code and
	 * deinterlaces it from the data codewords.
	 * </p>
	 * 
	 * @param dataStream Raw data stream read
	 * @param dataLen Length of the deinterlaced data stream
	 * @param dataBlocks Array containing the number of codewords in each data block
	 * @return A list containing the deinterlaced error correction bytes
	 * @see #getData
	 * @see QRErrorCorrection#getBlockData
	 */
	public static List <Byte> getErrorStream(List <Byte> dataStream, int dataLen, byte[] dataBlocks) {
		List <Byte> ecWords = new ArrayList <Byte> ();
		
		// Deinterlace error correction codewords
		for(int i = 0; i < dataBlocks.length; i++) {
			for(int j = 0; j < ((dataStream.size() - dataLen) / dataBlocks.length); j++) {
				ecWords.add(dataStream.get(i + j * dataBlocks.length + dataLen));
			}
		}
		
		return ecWords;
	}
	
	private static boolean maskDataBit(QRFinderPattern finderCenters, double modWidth, int format, int x, int y) {
		int codeI;
		int codeJ;
		byte maskPatt;
		
		// The last three bits of fmtClass denote the mask pattern
		maskPatt = (byte) (format & 0x07);
		codeI = (y - (int) (finderCenters.getPosition(QRFinderPattern.POS_UR).y - 3 * modWidth)) / (int) modWidth;
		codeJ = (x - (int) (finderCenters.getPosition(QRFinderPattern.POS_UR).x - 3 * modWidth)) / (int) modWidth;
		
		switch(maskPatt) {
		case 0x00:
			return (codeI + codeJ) % 2 == 0;
		case 0x01:
			return codeI % 2 == 0;
		case 0x02:
			return codeJ % 3 == 0;
		case 0x03:
			return (codeI + codeJ) % 3 == 0;
		case 0x04:
			return ((codeI / 2) + (codeJ / 3)) % 2 == 0;
		case 0x05:
			return (codeI * codeJ) % 2 + (codeI * codeJ) % 3 == 0;
		case 0x06:
			return ((codeI * codeJ) % 2 + (codeI * codeJ) % 3) % 2 == 0;
		case 0x07:
			return ((codeI * codeJ) % 3 + (codeI + codeJ) % 2) % 2 == 0;
		}

		return false;
	}
	
	private static boolean isFunctional(QRFinderPattern finderCenters, double modWidth, int versionInfo, int x, int y) {
		Point[] points = finderCenters.getArray();
		
		// Check upper-left finder pattern
		if(x <= (int) Math.round(points[0].x + 5 * modWidth) && y <= (int) Math.round(points[0].y + 5 * modWidth)) {
			return true;
		}
		
		// Check lower-left finder pattern
		if(x <= (int) Math.round(points[1].x + 5 * modWidth) && y >= (int) Math.round(points[1].y - 4 * modWidth)) {
			return true;
		}
		
		// Check upper-right finder pattern
		if(x >= (int) Math.round(points[2].x - 4 * modWidth) && y <= (int) Math.round(points[2].y + 5 * modWidth)) {
			return true;
		}
		
		// Check alignment pattern
		if (versionInfo > 1) {
			int centerX = (int) Math.round(points[0].x - 3 * modWidth + (18 + 4 * (versionInfo - 2)) * modWidth);
			int centerY = (int) Math.round(points[0].y - 3 * modWidth + (18 + 4 * (versionInfo - 2)) * modWidth);
			
			if(Math.abs(centerX - x) <= (2 * modWidth) && Math.abs(centerY - y) <= (2 * modWidth)) {
				return true;
			}
		}
		
		return false;
	}
	
	private static boolean isQuietZone(QRFinderPattern finderCenters, double modWidth, int x, int y) {
		Point[] points = finderCenters.getArray();
		
		if(x < (int) Math.round(points[0].x - 3 * modWidth) || x > (int) Math.round(points[2].x + 3 * modWidth)) {
			return true;
		}

		if(y < (int) Math.round(points[0].y - 3 * modWidth) || y > (int) Math.round(points[1].y + 3 * modWidth)) {
			return true;
		}
		
		return false;
	}
}
